iT邦幫忙

2022 iThome 鐵人賽

DAY 30
1
Modern Web

真的好想離開 Vue 3 新手村 feat. CompositionAPI系列 第 30

Day 30: Vue 3 響應式原理 - effect 如何響應 (無敵簡化版)

  • 分享至 

  • xImage
  •  

註:本篇屬於沒有很重要,但我很好奇系列

這篇不會著重在原始碼,主要是介紹 Vue 是用什麼概念去蒐集依賴。
主要學習資源是 Vue Mastery、官網文件和部份文章解析,本篇的程式碼幾乎都是模擬程式碼,來源是官方文件或官方課程,不是我自己寫的,請大家放心XD。

開始前建議閱讀下列文章:
Day 10: 從原生 JS 理解 Vue 3 響應式基礎 - reactive & ref (上)
Day 11: 從原生 JS 理解 Vue 3 響應式基礎 - reactive & ref (下)

前言

還記得我們在 Day 10 和 11 提到 ref 和 reactive 的原理,會分別透過物件的 setter/getter 和 Proxy 去攔截對物件屬性的讀取和寫入。

而 computed 和 watcher 會「依賴」響應式數據 (ref 和 reactive),在響應式數據改變後,去執行他們的工作內容,computed 會重新計算屬性值,watcher 則會執行傳入的 callback 函式。


但這一切究竟是怎麼連在一起的?

這就是今天文章的主題:

「Vue 如何在某個響應式狀態改變後,重新執行相關的函式/effect?」

  • 相關的函式是指什麼?
    主要指 computed 和 watcher 的工作內容,分別是重新計算結果和執行回呼函式。

答案就藏在 ref 和 reactive 的 getter 和 setter 內,也就是 track()trigger() 的部份。
今天將 track()trigger() 的部份補足,應該就可以把 Vue 響應式概念兜起來;所以,希望還不了解 ref 和 reactive 的人先去看 Day 10、11 的文章再回來。(到底要安利幾次哈哈)

// 官網示意程式碼
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      track(target, key) //here
      return target[key]
    },
    set(target, key, value) {
      target[key] = value
      trigger(target, key) //here
    }
  })
}
function ref(value) {
  const refObject = {
    get value() {
      track(refObject, 'value') //here
      return value
    },
    set value(newValue) {
      value = newValue
      trigger(refObject, 'value') //here
    }
  }
  return refObject
}

computed 和 watcher 都是「effect」

在開始之前,想先給大家一個概念:computed 和 watcher 都是一種「effect」。

所以說,effect 是什麼?
在框架中,「effect」通常指「side effect」,翻成中文是「副作用」的意思。副作用聽起來像是疫苗打完會頭痛、拉肚子有夠糟糕?
其實,在 functional programming 的定義中,「side effect」是指這個函式不是 pure function

嗯...不 pure 聽起來不太好?感覺髒髒的...?

Nonono,框架跟真實網頁開發本身就充滿必要的「副作用」,舉例來說:在 Vue 框架中,「某個狀態依賴其他狀態做響應式的改變」,就是一種 side effect 啊!

以下面的程式碼為例:

const numA = ref(20);
const numB = ref(5);

const numC = computed(() => numA.value * numA.value - 100)

numA.value = 100;

computed 會根據 numAnumB 值的改變,去重新計算 numC 的值。
也就是說,在我們改動 numA 狀態時,竟然也會讓 numC = numA.value * numA.value - 100 重新跑一次,這件事本身就不 pure,對吧?

所以,computed 本身就是一種 (side) effect。


因為 Vue 文件中沒有特別定義 effect 這個詞,所以借用 React 文件中的說明,如下:

Data fetching, setting up a subscription, and manually changing the DOM in React components are all examples of side effects. Whether or not you’re used to calling these operations “side effects” (or just “effects”), you’ve likely performed them in your components before. – React

像這樣資料或狀態響應式的改變,類似上面所說的 setting up a subscription,「訂閱一個狀態」,在他改變的時候去做其他事情(改變其他狀態等等)。

所以 computed、watch 和 watchEffect 都是一種 effect;接下來會用 effect 來代稱這些 Vue 響應式 API。


正文

先複習今日文章核心:「Vue 如何在某個響應式狀態改變後,重新執行相關的函式?」

首先要解決的是:Vue 怎麼知道某個響應式狀態改變後,要重新執行哪些函式/effect

如何儲存響應式狀態和 effect

Vue 之所以能知道「響應式狀態」和「effect」之間的依賴關係,是因為他會創造一個對應表,用來紀錄響應式物件、屬性和 effect 之間的關係。

以下面的程式碼為範例,Vue 會將 product (響應式物件)、pricequantity (屬性)、computedtotal 和 watcher (effect) ,用下下圖的結構儲存起來:

const product = reactive({ price: 5, quantity: 2 });
const total = computed(() => product.price * product.quantity);
watch( product.price, () => console.log("price changed!"))

說明

  • targetMap
    • 型別:WeakMap
      • key 為物件
      • 弱引用,在 key(物件)沒有在其他地方被參照時,就會讓垃圾回收機制處理掉,能避免 memory leak
    • key - 響應式物件(reactive 和 ref)
    • value - depsMap
  • depsMap
    • 型別:Map
      • key 可以是任何型別
    • key - 響應式物件屬性
    • value - dep(相關 effect 的 Set)
  • dep
    • 型別:Set
      • 避免重複儲存 effect(之後就會重複執行)
    • 內容項目為 effect,如:computed 和 watcher

接下來要看 ref 和 reactive 的 getter - track() 和 setter - trigger() 都做了什麼?就可以知道響應式狀態和 effect 之間如何作用。

track

每次在執行 effect 時,只要讀取響應式屬性,就會觸發 getter 裡面的 track(),那 track() 裡面在幹麻呢?其實就是把資料都填進上面的資料結構裡。

// antfu 文章 + 官網示意程式碼
let activeEffect;

const track = (target, key) => {
  if (tracking && activeEffect)
    targetMap.get(target).key(key).push(activeEffect)
}

以下面的程式碼來走一次流程:

const product = reactive({ price: 5, quantity: 2 });
const total = computed(() => product.price * product.quantity);
watch( product.price, () => console.log("price changed!"))

在執行 total 的 computed effect 過程中,讀取 product.priceproduct.quantity 的時候,會觸發 getter,並執行內部的 track()

  • 重點track 會去找現在是在執行哪個 effect(activeEffect),這個 effect 用到「我」這個屬性了欸,代表這個函式就是要儲存起來的 effect,因為「我」每次改變的時候,這個 effect 也應該重新執行一次

同理,在註冊 watch 的時候,也會讀取 product.price 並觸發 getter,這個 watch effect 也會被存到對應的 dep 內。

當然如果寫入的時候,發現對應的 targetMapdepsMapdep 還沒被建立,會新增資料把對應的物件、屬性和 effect 存進去;這裡著重在說明運行概念,就沒有把這部份的邏輯寫出來。

trigger

在了解 track 的工作之後,trigger 的工作就很好想像了,就是在每次屬性的 setter 被觸發時,會把儲存起來的 effect 通通執行一遍

模擬程式碼如下:

// antfu 文章示意程式碼
const trigger = (target, key) => {
  targetMap.get(target).key(key).forEach(effect => effect())
}

以下面的程式碼來走一次流程:

const product = reactive({ price: 5, quantity: 2 });
const total = computed(() => product.price * product.quantity);
watch( product.price, () => console.log("price changed!"))

product.price = 100;
  1. 在執行第 4 行的 product.price = 100; 時,會觸發 product proxy 屬性的 setter,並執行內部的 trigger()
  2. trigger() 會將屬性對應的 effect 集合依序執行一遍。
    • total computed 屬性內的 getter function 會被重新執行,也就得到響應更新後的值。
    • watcher 執行回呼函式,印出 "price changed!"

effect

現在我們知道 Vue 如何建立響應式狀態和 effect 之間的關聯,還剩下一個問題,Vue 怎麼知道現在在執行的是哪個 effect?
(其實也可能不只一個,但我今天就只有要講這三個部份XD)

// antfu 文章 + 官網示意程式碼
let activeEffect; //這是誰?????

const track = (target, key) => {
  if (tracking && activeEffect)
    targetMap.get(target).key(key).push(activeEffect)
}

這個部份的答案就要從 computed 和 watcher 裡面找,而在 Vue 3 這部份是基於 effect 這個 API 來實現的。

// antfu 文章示意程式碼
const effect = (fn) => {
  const effect = function () { fn() }
  enableTracking()
  activeEffect = effect
  fn()
  resetTracking()
  activeEffect = undefined
}

傳進 computedwatchwatchEffect 的回呼函式會被傳入 effect() ,執行之前,會先將當前 effect 函式賦值給 activeEffect 這個全域變數。

track() 執行期間就可以知道當前觸發 getter 的 activeEffect

小結

這大概是 Vue 3 響應式運作的原理,不知道寫得好不好懂XD

其實本來是想針對 computed 和 watcher 去研究,研究他們怎麼知道自己要重新執行。
但這整件事建構在 Vue 3 響應式模組上,整個程式碼架構太龐大,很難單獨看他們的程式碼去研究,所以轉為試著說明 Vue 響應式模組的運作原理,一併補足之後 reacitveref 響應式運作的後半段。

網路上蠻多人研究這方面的原始碼,大家好奇的話可以再去找找看!
如果還沒走火入魔,但又覺得這篇文章不夠清楚,蠻推薦去看看我底下附的參考資料,但我當初分開看都還是有點疑惑的地方,是最後全部加在一起後,終於比較了解。

後記

寫到一半突然在想,這裡是誰、我是哪裡?

這真的是一個方向莫名其妙的系列:)
主要還是想滿足自己的好奇心,難怪人家都說好奇心可以殺死一隻貓。

參考資料

原始碼連結

為了很勤奮的人們,把相關原始碼連結標在下面:

因為上面的程式碼都是示意,有的地方命名會和原始碼中有些微不同,但幾乎都能看得出來,例如:ref 裡面的 track 已經被包裝成 trackEffects
trackEffects 是基於 effect.ts 裡的 track()trigger() 進行實作。


上一篇
Day 29: Vue 響應式基礎 - watch & computed 不踩坑
下一篇
完賽心得 - 離開新手村了嗎?
系列文
真的好想離開 Vue 3 新手村 feat. CompositionAPI31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言